#include <fstream>

#define CL_USE_DEPRECATED_OPENCL_1_1_APIS
#include <CL/cl.hpp>
#include "opencl_intersect_device.h"
#include "primitive_funcs.h"

#include <iostream>

static cl::Context ray_intersect_context_;
static cl::Program ray_intersect_program_;
static std::vector<cl::Device>  ray_intersect_devices_;

static cl::Buffer ray_intersect_shape_data;
static cl::Buffer ray_intersect_accelerator_data;
static cl::Buffer ray_intersect_primitives;
static const size_t MAX_CPU_BUFFER_SIZE = 8*1024;//0xffffffff;

struct IntersectDevicePrivateData{
	cl::Buffer ray_intersect_intersections;
	cl::Buffer ray_intersect_rays;
	size_t ray_tracing_work_group_size ;
	cl::Kernel ray_intersect_kernel_;
	
	cl::CommandQueue ray_intersect_command_queue_;
	std::queue<cl::Event> ray_intersect_event_queue_;
};



static void ReadSource(const char *filename, std::string *source_string)
{
	size_t size;
	char*  str;

	std::fstream f(filename, (std::fstream::in | std::fstream::binary));

	if(f.is_open())
	{
		std::streamsize fileSize;
		f.seekg(0, std::fstream::end);
		size = fileSize = f.tellg();
		f.seekg(0, std::fstream::beg);

		str = new char[size+1];
		if(!str)
		{
			f.close();
			return ;
		}

		f.read(str, fileSize);
		f.close();
		str[size] = '\0';
	
		*source_string = str;
		delete[] str;
	}
	else
	{
		std::cout << "\nFile containg the kernel code(\".cl\") not found. Please copy the required file in the folder containg the executable.\n";
	}
}
template<typename T>
static cl::Buffer CreateBuffer(cl::Context context, cl_mem_flags flag, const std::vector<T>& data)
{
	return cl::Buffer(context,flag,sizeof(T) * data.size(),(void*)&data[0]);
}

//for further information: https://github.com/ddemidov/vexcl/
void OpenCLIntersectDevice::InitializeKernel(const scene_info_memory_t& scene_info)
{
	static bool initialized = false;
	if(!initialized)
	{
		std::vector<cl::Platform> platforms;
		cl::Platform::get(&platforms);
		for(auto it = platforms.begin(); it != platforms.end() && !initialized; ++it)
		{
			cl_context_properties cprops[]={
				CL_CONTEXT_PLATFORM,
				(cl_context_properties)(*it)(),
				0
			};
			ray_intersect_context_ = cl::Context(CL_DEVICE_TYPE_GPU, cprops);
			ray_intersect_devices_ = ray_intersect_context_.getInfo<CL_CONTEXT_DEVICES>();
			if(ray_intersect_devices_.empty())continue;
			std::string source_string;
			ReadSource("ray_intersect_kernel.cl",&source_string);
			cl::Program::Sources ray_intersect_sources(1,std::make_pair(source_string.c_str(),0));
			ray_intersect_program_ = cl::Program(ray_intersect_context_,ray_intersect_sources);
			if (CL_SUCCESS != ray_intersect_program_.build(ray_intersect_devices_,"-I. "))
				std::cout<<"cl file build failed"<<std::endl;

			ray_intersect_shape_data = CreateBuffer(ray_intersect_context_, CL_MEM_READ_ONLY| CL_MEM_USE_HOST_PTR, scene_info.shape_data);
			ray_intersect_accelerator_data = CreateBuffer(ray_intersect_context_, CL_MEM_READ_ONLY| CL_MEM_USE_HOST_PTR, scene_info.accelerator_data);
			ray_intersect_primitives = CreateBuffer(ray_intersect_context_, CL_MEM_READ_ONLY| CL_MEM_USE_HOST_PTR, scene_info.primitives);

			initialized = true;
		}
	}
}
OpenCLIntersectDevice::OpenCLIntersectDevice(const scene_info_memory_t& scene_info, 
	const size_t ray_buffer_size,
	const size_t max_queue_size)
	:ray_buffer_size_(ray_buffer_size),max_queue_size_(max_queue_size),scene_info_memory_(scene_info)
{
	private_data_ = new IntersectDevicePrivateData();

	InitializeKernel(scene_info);

	private_data_->ray_intersect_kernel_ = cl::Kernel(ray_intersect_program_,"ray_intersect");
	private_data_->ray_intersect_kernel_.setArg(2,ray_intersect_shape_data);
	private_data_->ray_intersect_kernel_.setArg(3,ray_intersect_accelerator_data);
	private_data_->ray_intersect_kernel_.setArg(4,ray_intersect_primitives);
	private_data_->ray_intersect_kernel_.setArg(5,(unsigned int)scene_info.primitives.size());
	private_data_->ray_intersect_kernel_.getWorkGroupInfo(ray_intersect_devices_[0],CL_KERNEL_WORK_GROUP_SIZE,&private_data_->ray_tracing_work_group_size);
	private_data_->ray_tracing_work_group_size = min(private_data_->ray_tracing_work_group_size,64);	
	private_data_->ray_intersect_kernel_.setArg(7,sizeof(cl_int) * MAX_BVH_DEPTH * private_data_->ray_tracing_work_group_size,NULL);
	
	private_data_->ray_intersect_command_queue_ = cl::CommandQueue(ray_intersect_context_,ray_intersect_devices_[0],0);
}
OpenCLIntersectDevice::~OpenCLIntersectDevice()
{
	delete private_data_;
}
std::shared_ptr< std::vector<ray_t> > OpenCLIntersectDevice::CreateRayBuffer(const size_t ray_buffer_size)
{
	ray_buffer_size_ = ray_buffer_size;
	return std::shared_ptr<std::vector<ray_t> >(new std::vector<ray_t>(ray_buffer_size));
}

void OpenCLIntersectDevice::PushRayBuffer(const std::shared_ptr<std::vector<ray_t> > ray_buffer)
{
	if(ray_buffer->empty())return;
	intersection_buffer_queue_.push(std::vector<intersection_t>());
	ray_buffer_queue_.push(ray_buffer);

	if (ray_buffer->size() < MAX_CPU_BUFFER_SIZE)
		return;//using cpu

	private_data_->ray_intersect_event_queue_.push(cl::Event());

	cl::Event& event = private_data_->ray_intersect_event_queue_.back();
	std::vector<intersection_t>& intersections = intersection_buffer_queue_.back();
	std::vector<ray_t>& rays = *ray_buffer_queue_.back();

	intersections.resize(rays.size());

	private_data_->ray_intersect_intersections = CreateBuffer(ray_intersect_context_,CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,intersections);
	private_data_->ray_intersect_rays = CreateBuffer(ray_intersect_context_,CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,rays);

	private_data_->ray_intersect_kernel_.setArg(1,private_data_->ray_intersect_rays);
	private_data_->ray_intersect_kernel_.setArg(0,private_data_->ray_intersect_intersections);
	private_data_->ray_intersect_kernel_.setArg(6,(unsigned int)ray_buffer->size());

	size_t global_threads = rays.size();
	if (global_threads % private_data_->ray_tracing_work_group_size!= 0)
		global_threads = (global_threads / private_data_->ray_tracing_work_group_size + 1) * private_data_->ray_tracing_work_group_size;
	size_t local_threads = private_data_->ray_tracing_work_group_size;
	private_data_->ray_intersect_command_queue_.enqueueNDRangeKernel(private_data_->ray_intersect_kernel_,cl::NullRange,
		cl::NDRange(global_threads),cl::NDRange(local_threads));

	private_data_->ray_intersect_command_queue_.enqueueReadBuffer(private_data_->ray_intersect_intersections,CL_FALSE,0,
	intersections.size()*sizeof(intersections[0]),&intersections[0],NULL,&event);
	private_data_->ray_intersect_command_queue_.flush();
	pop_mode_ = PopModeEnqueue;
}
bool OpenCLIntersectDevice::PopIntersectionBuffer(std::shared_ptr<std::vector<ray_t> >* ray_buffer, std::vector<intersection_t>* isects)
{
	//CPU
	/*
	isects->clear();
	if(ray_buffer_queue_.empty())
	{
		pop_mode_ = PopModeForceWait;
		return false;
	}
	*ray_buffer = ray_buffer_queue_.front();
	for(size_t i = 0;i < (*ray_buffer)->size(); ++i)
	{
		intersection_t isect;
		ray_t ray;
		rassign(ray,(**ray_buffer)[i]);
		isect.primitive_idx = 0xffffffff;
		unsigned int ret = intersect(&scene_info_memory_.accelerator_data[0],&scene_info_memory_.shape_data[0],&scene_info_memory_.primitives[0],scene_info_memory_.primitives.size(),
			&ray,&isect);
		printf("hit: %d\t",ret);
		rassign((**ray_buffer)[i],ray);
		isects->push_back(isect);
	}
	intersection_buffer_queue_.pop();
	ray_buffer_queue_.pop();
	ray_intersect_event_queue_.pop();
	return !isects->empty();
	*/

	//GPU
	isects->clear();
	if(ray_buffer_queue_.empty())
	{
		pop_mode_ = PopModeForceWait;
		return false;
	}

	//cpu hybrid
	if(ray_buffer_queue_.front()->size() < MAX_CPU_BUFFER_SIZE )
	{
		*ray_buffer = ray_buffer_queue_.front();
		isects->reserve((*ray_buffer)->size());
		for(size_t i = 0;i < (*ray_buffer)->size(); ++i)
		{
			intersection_t isect;
			ray_t ray;
			rassign(ray,(**ray_buffer)[i]);
			isect.primitive_idx = 0xffffffff;
			unsigned int ret = intersect(
				&scene_info_memory_.accelerator_data[0],
				&scene_info_memory_.shape_data[0],
				&scene_info_memory_.primitives[0],
				static_cast<unsigned int>(scene_info_memory_.primitives.size()),
				&ray,&isect);
			rassign((**ray_buffer)[i],ray);
			isects->push_back(isect);
		}
		intersection_buffer_queue_.pop();
		ray_buffer_queue_.pop();
		pop_mode_ = PopModeForceWait;
		return !isects->empty();
	}
	//GPU continue
	cl::Event& event = private_data_->ray_intersect_event_queue_.front();
	cl_int execute_status = -1;

	if(private_data_->ray_intersect_event_queue_.size() > max_queue_size_ || pop_mode_ == PopModeForceWait)
	{
		event.wait();
		execute_status = CL_COMPLETE;
		//printf("wait!\n");
	}
	else{
		//do{
		event.getInfo(CL_EVENT_COMMAND_EXECUTION_STATUS,&execute_status);
		//}while(execute_status != CL_COMPLETE);//block
		//printf("not wait %d!\n",execute_status == CL_COMPLETE);
	}
	if(execute_status == CL_COMPLETE )
	{
		std::vector<intersection_t>& intersections = intersection_buffer_queue_.front();
		*ray_buffer = ray_buffer_queue_.front();
		isects->swap(intersections);
		intersection_buffer_queue_.pop();
		ray_buffer_queue_.pop();
		private_data_->ray_intersect_event_queue_.pop();
		pop_mode_ = PopModeForceWait;
		return true;
	}
	else
	{
		*ray_buffer = CreateRayBuffer(ray_buffer_size_);
		pop_mode_ = PopModeForceWait;
		return false;
	}
	/*
	isects->resize(test_ray_buffer->size());
	cl_scene_info_t cl_scene_info = as_cl_scene_info(scene_info_memory_);
	for(size_t i = 0;i < test_ray_buffer->size(); ++i)
	{
		ray_t ray;
		const size_t gid = i;
		rassign(ray,(*test_ray_buffer)[gid]);
		intersection_t isect;
		isect.primitive_idx = 0xffffffff;
		intersect(cl_scene_info.accelerator_data, cl_scene_info.shape_data, cl_scene_info.primitives, 
			cl_scene_info.primitive_count,&ray,&isect);
		(*isects)[gid] = isect;
	}
*/
}